今天來研究如何處理使用者互動行為,這個專案會用到 click、drag 等動作,讓記事進入編輯狀態,還能透過拖曳來改變 list 的順序。所以首先要先知道如何取得click、drag的狀態。
使用者和元件的互動最常用的就是 click,在 compose 中可以從 Modifier.clickable 來判斷使用者是否點擊了按鈕,並且執行在 lambda 中的代碼。直接使用 clickable 的好處是不需要知道點擊事件是由鍵盤操作還是畫面點擊執行的、也不需要知道背後的實作原理就能直接使用。
然而本專案需要知道drag 這個動作,因此要用到Interaction 。當使用者和介面互動時,系統會產生多個Interaction 事件來表達使用者行為。如果要從 Composable 中取得 Interaction 就可以觀察參數中有沒有InteractionSource 。
這次用的 Card 就有 interactionSource ,我們可以把預設值 remember { MutableInteractionSource() } 拿來使用。
@ExperimentalMaterial3Api
@Composable
fun Card(
    onClick: () -> Unit,
		//...
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
		//...
) {
		//...
}
首先建立變數 interactionSource , 其值就是 remember { MutableInteractionSource() } 。
再來用 interactionSource 的 function 來獲取手勢的狀態,總共有這幾種
collectIsPressedAsState() :是否按著元件
collectIsDraggedAsState() :是否拖曳著元件
collectIsFocusedAsState() :是否焦點在元件上
collectIsHoveredAsState() :是否按著元件不動(懸停)
回傳型態是 State<Boolean> 。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
    val interactionSource = remember { MutableInteractionSource() }
    val isPressed by interactionSource.collectIsPressedAsState()
    val isDragged by interactionSource.collectIsDraggedAsState()
		val isFocused by interactionSource.collectIsFocusedAsState()
    val isHovered by interactionSource.collectIsHoveredAsState()
		//...
這裡我會遇到一個問題,那就是我同時監聽長按和拖曳的狀態,Compose 會執行大量的重複動作,而且不能確保正確的互動順,這時就要改成InteractionSource 。
InteractionSource 用的是 Flow API,要取得最新的資料就需要用到 collect。
在 collect 中使用 when 確保互動是依照一定順序判斷使用者的互動,並且把正在進行的互動加到 interactions list 中 。
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}
除了加入之外,當使用者放開元件,也要從 list 中移除狀態。
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}
整理好後我就可以 interactions 檢查是否有值,就能知道目元件正在 Pressed 或 Dragged 。
val isPressedOrDragged = interactions.isNotEmpty()
因為動作狀態存在 list 的關係,還能看出動作的順序, list 的最後一項就是最新的動作。
val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}
未完待續...
今日運動
腳還在酸 休息